Back to Community Blog

Help reduce global customer loss by using a dual payment provider strategy with PayPal and Stripe

authorImage

Eddie Jaoude

May 26, 2026

10 min read

featuredImage



A percentage of your customers want to use PayPal, so why are you leaving money on the table using just one payment provider?

It is not uncommon for developers to use several payment providers over the years. These range from the “big players” such as PayPal and Stripe, to lesser known platforms such as LemonSqueezy, Polar and Paddle. Every platform has its pros and cons, so what if we could decouple from one single payment provider and easily integrate with multiple payment platforms which would bring developers and customers more advantages?

Well we can. Developers no longer need to restrict themselves to one payment platform which they select at the outset and have to remain with it forever. Instead developers can design their checkout so they can use more than one provider behind a clean abstraction! In this blog post I will go through the reasons why, benefits and challenges of using a multi-payment provider in your app, such as PayPal and Stripe.

 

Why?

Most users will be thinking "Why should I have two payment providers?". There are multiple reasons:

  • Redundancy: We often talk about redundancy and uptime but rarely when it comes to payment providers. Even 99.9% uptime means 8 hours of downtime a year. If that happens during a busy period, such as Black Friday, this can have disastrous consequences to a business. The "nightmare scenario" of what happens when your only payment provider goes down is particularly scary given that it is impossible to make the switch to another payment provider quickly.
  • Trust: PayPal is recognised by consumers all over the world. Many consider it synonymous with reliability and dependability.
  • Geographic: One provider may outperform another depending on country, currency, or payment method. By choosing to integrate two payment providers, you are broadening the scope of what can be achieved as you are not subject to one authorisation rate and local payment preference.
  • User Choice: Offering both "Pay with Card" via Stripe or Pay with "PayPal buy button" empowers the customer to decide how they want to proceed. Customers like having choices.
  • Failover Routing: If the customer's first payment option returns a 5xx error or a specific "Processor Declined" code, the app can automatically trigger a fallback to the other provider.
  • Smart Routing: Using metadata (such as IP address or currency) selects the payment provider with the lowest fee or highest authorization rate for that specific region.

 

Challenges

You may be convinced by the benefits mentioned above. However a deciding point will be how much extra work integrating two payment providers represents, and whether there are significant complications when trying to do so. Here are some challenges you could face:

  • Normalizing objects: mapping a Stripe and PayPal transaction record in your own database structure.
  • Checkout flow: a user flow that can handle both payment providers SDK API's in a standard way.
  • Webhooks management: an endpoint that can parse events from both payment providers and update your order status consistently. The endpoint parsing the Webhooks from a single payment provider usually has a basic switch statement to match the event type. This alone would not be enough and an extra check to which payment provider sent the Webhook would be needed.

These challenges to handle multiple payment providers might sound more complicated than they are. Fundamental Design Patterns that have been used in software engineering for decades can help solve these challenges to keep our project's code clean and flexible.

 

Architecture

What are Design Patterns? These are common solutions to frequent occurring problems in software engineering. They resemble pre-made blueprints that you can customize to solve a recurring design problem in your code. Design Patterns are not concrete implementations of code (like a library), but a general concept for solving a particular problem.

Design patterns usually fall into 3 categories: creational, structural, behavioral.

  • Creational Patterns: provide mechanisms that increase flexibility and reuse of existing code.
  • Structural Patterns: explain how to assemble code into larger structures, while keeping these structures flexible and efficient.
  • Behavioral Patterns: take care of effective communication and the assignment of responsibilities between code.

Even when the project will handle multiple payment providers, start building with one payment provider, but architect for two or more from day one. This is where Design Patterns really help, allowing us to create reusable abstractions. For the multiple payment providers there are two areas that will need decoupling, Checkout and Webhook. This is where the Adapter Design Pattern would solve these challenges.

 

Adapter Pattern

The Adapter Pattern is a Structural Design Pattern that has a very descriptive name. Think of it like a travel adapter. You don't buy a new phone or hair dryer every time you travel to another country that has different wall plugs - you use a travel adapter. The same occurs in code. We have incompatible contracts in code that need to collaborate together in our project, in this specific case the Stripe and PayPal SDKs expose different APIs. The Adapter Design Pattern creates a layer that lets the Checkout or Webhook communicate via a single interface. This allows each payment provider its own specific adapter to handle the custom translation, whilst still adhering to the agreed interface contract.

 

Diagram

Let’s take a look at the Adapter Design Pattern with a diagram and then actual code. In this diagram I will use the travel adapter situation that highlights the Adapter Pattern clearly.

The 2 pin plug does not fit in the 3 pin socket. By using an adapter, this will work as the adapter will accept the 2 pin plug and itself has 3 pins to fit into the 3 pin wall socket.



Example code

The code blocks below use Typescript classes, but can be applied to any language and even functional code. In this example you will see how we can normalize different payment provider SDKs behind one contract, so these can work together.

Let's start by defining the interfaces

The following interfaces show the core idea allowing the checkout service to stay independent of each SDK's details.

ChargeRequest: describes the normalized payment data CheckoutService needs to request a charge.

interface ChargeRequest {

  amountCents: number;

  currency: Currency;

  orderId: string;

 }

ChargeResult: represents the provider agnostic result returned after attempting a payment.

interface ChargeResult {

  provider: "paypal" | "stripe";

  transactionId: string;

  approved: boolean;

}

PaymentProvider: defines the shared contract every payment provider Adapter must implement and adhere to.

interface PaymentProvider {

  charge(request: ChargeRequest): Promise<ChargeResult>;

  refund(transactionId: string, amountCents?: number): Promise<void>;

}

Implementations

Each payment provider's SDK gets an adapter that converts internal requests into provider native calls.

PayPalAdapter: adapts PayPal's SDK API into the same common PaymentProvider contract.

class PayPalAdapter implements PaymentProvider {

  constructor(private readonly paypal: PayPalSdk) {}



  async charge(request: ChargeRequest): Promise<ChargeResult> {

    const capture = await this.paypal.captureOrder({ /* ... */ });



    return {

      provider: "paypal",

      transactionId: capture.id,

      approved: capture.status === "COMPLETED",

    };

  }  

async refund(transactionId: string, amountCents?: number): Promise<void> {

    await this.paypal.refundCapture( /* ... */ );

  }

}

StripeAdapter: adapts Stripe's SDK API into the same common PaymentProvider contract.

class StripeAdapter implements PaymentProvider {

  constructor(private readonly stripe: StripeSdk) {}



  async charge(request: ChargeRequest): Promise<ChargeResult> {

    const intent = await this.stripe.createPaymentIntent({ /* ... */ });



    return {

      provider: "stripe",

      transactionId: intent.id,

      approved: intent.status === "succeeded",

    };

  }



  async refund(transactionId: string, amountCents?: number): Promise<void> {

    await this.stripe.createRefund({ /* ... */ });

  }

}

CheckoutService: orchestrates checkout flow while remaining independent of any specific provider SDK because the application logic only depends on the shared contract.

  class CheckoutService {

   constructor(private readonly provider: PaymentProvider) {}



  async processOrder(orderId: string, amountCents: number): Promise
{

    const result = await this.provider.charge({

      orderId,

      amountCents,

      currency: "USD",

   });

   if (!result.approved) {

     throw new Error(`Payment declined by ${result.provider}`);

   }



    return result.transactionId;

  }

 }

Example usage where providers can be swapped without changing the code in the CheckoutService.

const paypalCheckout = new CheckoutService(new PayPalAdapter(new PayPalSdk()));

const stripeCheckout = new CheckoutService(new StripeAdapter(new StripeSdk()));



const paypalTx = await paypalCheckout.processOrder("ORDER-1001", 2599);

const stripeTx = await stripeCheckout.processOrder("ORDER-1002", 2599);

This is the architectural win: checkout code does not care whether it is PayPal or Stripe under the hood.

Adding more payment providers

More payment providers can easily be added by implementing the PaymentProvider interface, which will enforce the rules of our original shared contract so that no matter the new SDK to our project it will work in the same way.

class PaddleAdapter implements PaymentProvider {

  constructor(private readonly paddle: PaddleSdk) {}



  async charge(request: ChargeRequest): Promise<ChargeResult> {

    // ...     return {

      provider: "paddle",

      transactionId: order.id,

      approved: order.status === "succeeded",

    };

  }  



async refund(transactionId: string, amountCents?: number): Promise<void> {

    // ...

  }

}

  image

Conclusions

A dual payment provider strategy is not about adding complexity for its own sake. It is about helping to protect conversion, improve resilience, and give customers payment options they trust.

The Adapter Pattern gives you a clean way to do this: one stable contract for your app and provider-specific encapsulated translation. Start simple with one provider adapter, add the second, and evolve routing based on real production data.

If checkout is a growth surface in your product, this architecture turns payments from a fragile dependency into a competitive advantage.